Skip to content

fix(a11y): make artwork lightbox triggers and interactive rows keyboard-accessible#69

Open
Copilot wants to merge 5 commits into
mainfrom
copilot/fix-accessibility-lightbox-triggers
Open

fix(a11y): make artwork lightbox triggers and interactive rows keyboard-accessible#69
Copilot wants to merge 5 commits into
mainfrom
copilot/fix-accessibility-lightbox-triggers

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 19, 2026

Summary

  • Three artwork lightbox triggers (NowPlayingPanel, AlbumDetailView, ArtistDetailView) were bare <div onClick> / <img onClick> — not focusable, not announced by screen readers, and broken for useModalA11y focus restoration (captured activeElement was <body>, not the trigger).
  • Broader audit of cursor-pointer patterns found the same gap on lyric-seek rows, queue rows, WrappedView artist rows, and all six track-table views.

Artwork triggers (NowPlayingPanel, AlbumDetailView, ArtistDetailView):

  • Replaced with <button type="button"> + aria-label={t("common.viewArtwork")} + focus-visible:ring-2 focus-visible:ring-emerald-500
  • Button is omitted (not disabled) when no artwork path — no dead Tab stops
  • ArtistDetailView: pointer-events-none overlay + pointer-events-auto edit-pencil preserved; pencil is a sibling of the new button, not nested inside it, so clicks don't cross-fire
  • useModalA11y round-trip now works: document.activeElement is the <button> on open → focus returns to it on close

Lyrics seek rows (LyricsPanel, FullscreenLyrics):

  • Added role="button", tabIndex={0}, onKeyDown (Enter/Space → seek) to synced <li> elements

WrappedView top-artists:

  • <li onClick><li><button>, matching the already-accessible top-albums pattern in the same component

QueuePanel:

  • QueueRow + SortableQueueItem: tabIndex, role="button", onKeyDown (Enter/Space → jump to track)

Track rows (LibraryView, PlaylistView, LikedView, AlbumDetailView, ArtistDetailView, GenreDetailView):

  • tabIndex={0} + onKeyDown (Enter/Space → play); focus ring uses focus-visible:ring-inset focus-visible:ring-emerald-500

i18n: common.viewArtwork added to all 17 locale files.

How I tested

  • bun run lint and bun run typecheck both pass cleanly
  • Manually verified the three artwork buttons are reachable via Tab and activate on Enter/Space
  • Confirmed the edit-pencil in ArtistDetailView still opens the image picker independently of the lightbox button

Screenshots / clips

Checklist

  • Title uses Conventional Commits (type(scope): subject, kebab-case scope)
  • bun run lint + bun run typecheck pass locally
  • cargo check --manifest-path src-tauri/Cargo.toml --all-targets passes locally
  • If UI strings changed: every locale in src/i18n/locales/ updated
  • If cross-cutting pattern changed: CLAUDE.md and docs/ updated
  • Breaking change? Called out in the summary above

Copilot AI changed the title [WIP] Fix accessibility for artwork lightbox triggers fix(a11y): make artwork lightbox triggers and interactive rows keyboard-accessible May 19, 2026
Copilot AI requested a review from InstaZDLL May 19, 2026 12:08
@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented May 19, 2026

Not up to standards ⛔

🔴 Issues 5 high · 11 medium

Alerts:
⚠ 16 issues (≤ 0 issues of at least minor severity)

Results:
16 new issues

Category Results
BestPractice 11 medium
5 high

View in Codacy

🟢 Metrics 6 complexity · -2 duplication

Metric Results
Complexity 6
Duplication -2

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@InstaZDLL InstaZDLL force-pushed the copilot/fix-accessibility-lightbox-triggers branch from c341b8e to fdee4d5 Compare May 19, 2026 12:56
@github-actions github-actions Bot added scope: frontend React/Vite frontend (src/) scope: i18n Translations (src/i18n/) type: fix Bug fix size: l 200-500 lines size: xl > 500 lines and removed size: l 200-500 lines size: xl > 500 lines labels May 19, 2026
@InstaZDLL InstaZDLL force-pushed the copilot/fix-accessibility-lightbox-triggers branch from 64fb2ba to 04c6e0a Compare May 20, 2026 20:02
Copilot AI and others added 4 commits May 20, 2026 22:28
Follow-up to Copilot's a11y pass on this branch. Codacy raised two
classes of findings:

1. Ten MEDIUM "JSX attribute is specific to React" — PMD running on
   `.tsx`/`.jsx` and flagging `className` as if it were JSP. PMD
   targets Java / Apex / PLSQL / JSP — none of which exist in this
   repo. Drop PMD (and the Java runtime it pulled in) from
   `.codacy/codacy.yaml`; eslint already covers JS/TS, Opengrep +
   Trivy cover security, Lizard covers complexity.

2. Nine HIGH `jsx-a11y/*` findings — `<li>` / `<div>` non-interactive
   elements carrying `tabIndex` + `role="button"`. Split the fix two
   ways:

   - Lyrics rows (`LyricsPanel`, `FullscreenLyrics`) and the now-
     playing `QueueRow` (interactive variant was dead code anyway):
     refactor into real `<button>` elements. `onKeyDown` becomes
     unnecessary — `<button>` natively handles Enter and Space. Net
     code reduction.
   - `SortableQueueRow` and the six `TrackTable` rows: contain inner
     `<button>` elements (drag handle, like, more-options), so the
     outer can't be a `<button>` without producing nested-button
     invalid HTML. Keep the `<div tabIndex role="button">` pattern
     with bare `// eslint-disable-next-line` + comment explaining
     why the compound is intentional; keyboard activation still
     works end-to-end.

The bare disable directives target jsx-a11y rules that Codacy runs
server-side but that we don't load locally (no
`eslint-plugin-jsx-a11y` dep). Set linterOptions.reportUnusedDisableDirectives
to "off" in `eslint.config.js` so eslint v9 doesn't flag them as
unused locally.
Codacy runs its a11y checks via a cloud-side analyzer that doesn't read
the repo's eslint config and doesn't honor inline `eslint-disable`
directives. The seven bare `// eslint-disable-next-line` lines added in
the previous commit were therefore load-bearing for nothing and only
produced "Unused eslint-disable directive" warnings locally. Drop them
along with the matching `reportUnusedDisableDirectives: "off"` opt-out
in `eslint.config.js`. The explanatory comments above each compound
row stay — they still document why the `<div tabIndex role="button">`
pattern is intentional for rows that contain inner buttons.
The Codacy CLI install script leaked into the previous commit despite
being declared in `.codacy/.gitignore` — that file is itself untracked,
so its rules only apply on machines that already have it. Move the
exclusion rules to the tracked root `.gitignore` so a fresh checkout
doesn't accumulate the same machine-local artefacts.
@InstaZDLL InstaZDLL force-pushed the copilot/fix-accessibility-lightbox-triggers branch from 04c6e0a to a988812 Compare May 20, 2026 20:28
@InstaZDLL InstaZDLL marked this pull request as ready for review May 20, 2026 20:35
@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 20, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0)

Grey Divider


Action required

1. Bubbled keydown plays track 🐞 Bug ≡ Correctness
Description
Multiple track-table rows (including SortableQueueRow) attach an Enter/Space onKeyDown that
plays/jumps to a track but don’t ensure the row itself is the key event target, so keyboard
activation on nested interactive elements (e.g., like/menu/ArtistLink/AlbumLink buttons and the
drag-handle button) can bubble up and trigger unintended playback/jumps. This breaks keyboard
interaction by causing unexpected track changes while operating in-row controls.
Code

src/components/views/LibraryView.tsx[R1106-1114]

Evidence
The cited row containers are focusable and implement onKeyDown handlers that react to Enter/Space
to play or onJump, while the nested interactive elements inside those rows only prevent
propagation for mouse interactions (e.g., click/doubleClick). Because nothing blocks keydown
bubbling (either via a row-level e.target !== e.currentTarget guard or keydown stopPropagation on
the nested controls), pressing Enter/Space on an in-row button—such as the QueuePanel drag
handle—can propagate to the row handler and invoke playback/jump unexpectedly.

src/components/views/LibraryView.tsx[1100-1114]
src/components/views/LibraryView.tsx[1220-1270]
src/components/common/ArtistLink.tsx[23-73]
src/components/common/AlbumLink.tsx[13-45]
src/components/views/ArtistDetailView.tsx[578-383]
src/components/layout/QueuePanel.tsx[525-553]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Several track-table row containers (including `SortableQueueRow`) handle `Enter`/`Space` in `onKeyDown` to play/jump to the row’s track, but the handler can run even when the key event originated from a nested interactive element (e.g., like button, add-to-playlist/menu button, ArtistLink/AlbumLink buttons, or the queue drag-handle `<button>`). Because nested controls typically only stop propagation for mouse events, keyboard `keydown` can still bubble to the row and trigger unintended playback/jumps.

## Issue Context
These rows intentionally contain nested interactive controls (buttons/links), and the queue drag-handle already stops propagation for `click`/`doubleClick` but not for keyboard events. The row-level Enter/Space behavior should only trigger when the row itself is the focused/targeted element (or nested controls should explicitly prevent keydown propagation).

## Fix Focus Areas
- src/components/views/LibraryView.tsx[1104-1114]
- src/components/views/PlaylistView.tsx[1107-1118]
- src/components/views/LikedView.tsx[154-165]
- src/components/views/AlbumDetailView.tsx[472-482]
- src/components/views/ArtistDetailView.tsx[584-593]
- src/components/views/GenreDetailView.tsx[282-291]
- src/components/layout/QueuePanel.tsx[531-551]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Qodo Logo

@github-actions github-actions Bot added size: xl > 500 lines and removed size: l 200-500 lines labels May 20, 2026
@InstaZDLL InstaZDLL force-pushed the copilot/fix-accessibility-lightbox-triggers branch from b3ad445 to a988812 Compare May 20, 2026 20:37
@github-actions github-actions Bot removed the size: xl > 500 lines label May 20, 2026
@github-actions github-actions Bot added the size: l 200-500 lines label May 20, 2026
Comment on lines +1106 to +1114
tabIndex={0}
onClick={(e) => onRowSelect(track, e)}
onDoubleClick={() => onPlayTrack(index)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onPlayTrack(index);
}
}}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Bubbled keydown plays track 🐞 Bug ≡ Correctness

Multiple track-table rows (including SortableQueueRow) attach an Enter/Space onKeyDown that
plays/jumps to a track but don’t ensure the row itself is the key event target, so keyboard
activation on nested interactive elements (e.g., like/menu/ArtistLink/AlbumLink buttons and the
drag-handle button) can bubble up and trigger unintended playback/jumps. This breaks keyboard
interaction by causing unexpected track changes while operating in-row controls.
Agent Prompt
## Issue description
Several track-table row containers (including `SortableQueueRow`) handle `Enter`/`Space` in `onKeyDown` to play/jump to the row’s track, but the handler can run even when the key event originated from a nested interactive element (e.g., like button, add-to-playlist/menu button, ArtistLink/AlbumLink buttons, or the queue drag-handle `<button>`). Because nested controls typically only stop propagation for mouse events, keyboard `keydown` can still bubble to the row and trigger unintended playback/jumps.

## Issue Context
These rows intentionally contain nested interactive controls (buttons/links), and the queue drag-handle already stops propagation for `click`/`doubleClick` but not for keyboard events. The row-level Enter/Space behavior should only trigger when the row itself is the focused/targeted element (or nested controls should explicitly prevent keydown propagation).

## Fix Focus Areas
- src/components/views/LibraryView.tsx[1104-1114]
- src/components/views/PlaylistView.tsx[1107-1118]
- src/components/views/LikedView.tsx[154-165]
- src/components/views/AlbumDetailView.tsx[472-482]
- src/components/views/ArtistDetailView.tsx[584-593]
- src/components/views/GenreDetailView.tsx[282-291]
- src/components/layout/QueuePanel.tsx[531-551]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Qodo follow-up on the a11y branch. Track-row + queue-row containers
handle Enter/Space to play / jump, but the handler also ran when the
key event originated from a nested control (like button, `+` button,
ArtistLink, AlbumLink, the drag-handle in QueuePanel). Nested buttons
stop propagation for clicks but not for keydown, so pressing Enter on
the like button also triggered the row's playback handler.

Gate every row handler on `e.target === e.currentTarget` so the
behaviour only fires when the row itself is the focused element.
Covered: LibraryView, PlaylistView, LikedView, AlbumDetailView,
ArtistDetailView, GenreDetailView, QueuePanel.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: frontend React/Vite frontend (src/) scope: i18n Translations (src/i18n/) size: l 200-500 lines type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

a11y: clickable artwork (lightbox + interactive rows) is not keyboard-accessible

2 participants